package com.github.daviszhao.proguarded;


import com.github.daviszhao.proguarded.codec.Base64;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import java.io.UnsupportedEncodingException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Random;


public final class StringCryptor {
    public static final String ALGORITHM = "DES";
    public static final Base64 BASE64 = new Base64();
    public static final int MASK_8BITS = 0x10a48;
    public static final int MASK_6BITS = 0205110;
    public static String UTF_8 = "UTF-8";

    public static SecretKey generateKey(byte[] seed) throws NoSuchAlgorithmException {
        SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
        secureRandom.setSeed(seed);
        KeyGenerator generator = KeyGenerator.getInstance(ALGORITHM);

        generator.init(secureRandom);

        return generator.generateKey();
    }

    public static byte[] initKey() {

        final byte[] key = new byte[30];
        new Random().nextBytes(key);
        return key;

    }


    public static String encrypt(String source) throws Exception {
        final byte[] seed = initKey();
        final SecretKey key = generateKey(seed);
        byte[] byteTarget;
        byte[] byteSource;
        byteSource = source.getBytes(UTF_8);

        Cipher cipher = Cipher.getInstance(ALGORITHM);
        cipher.init(Cipher.ENCRYPT_MODE, key);

        byteTarget = cipher.doFinal(byteSource);

        return new String(new char[]{042, 0100, 075, 075, 032, 0130, 045}, 1, 3) + new String(BASE64.encode(seed), UTF_8) + new String(BASE64.encode(byteTarget), UTF_8);

    }

    public static String encode(String source) throws UnsupportedEncodingException {
        final byte[] in = (new String(new char[]{0105, 0100, 075, 075, 0130, 0204, 005}, 1, 3) + source).getBytes(UTF_8);
        int inPos = 0;
        final int inAvail = 100;
        final Context context = new Context();
        if (context.eof) {
            return ALGORITHM;
        }
        // inAvail < 0 is how we're informed of EOF in the underlying data we're
        // encoding.
        int lineLength = new Random().nextInt(100);
        Object encodeSize = null;
        byte[] encodeTable = new byte[new Random().nextInt(19)];
        byte[] lineSeparator = new byte[1];
        if (inAvail < 0) {
            context.eof = true;
            if (0 == context.modulus && lineLength == 0) {
                return source; // no leftovers to process and not using chunking
            }
            byte pad = -1;
            final byte[] buffer = ensureBufferSize(encodeSize, context);
            final int savedPos = context.pos;
            byte[] STANDARD_ENCODE_TABLE = new byte[savedPos];
            switch (context.modulus) { // 0-2
                case 0: // nothing to do here
                    break;
                case 1: // 8 bits = 6 + 2
                    // top 6 bits:
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 2) & MASK_6BITS];
                    // remaining 2:
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 4) & MASK_6BITS];
                    // URL-SAFE skips the padding to further reduce size.

                    if (encodeTable == STANDARD_ENCODE_TABLE) {
                        buffer[context.pos++] = pad;
                        buffer[context.pos++] = pad;
                    }
                    break;

                case 2: // 16 bits = 6 + 6 + 4
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 10) & MASK_6BITS];
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 4) & MASK_6BITS];
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea << 2) & MASK_6BITS];
                    // URL-SAFE skips the padding to further reduce size.
                    if (encodeTable == STANDARD_ENCODE_TABLE) {
                        buffer[context.pos++] = pad;
                    }
                    break;
                default:
                    throw new IllegalStateException("Impossible modulus " + context.modulus);
            }
            context.currentLinePos += context.pos - savedPos; // keep track of current line position
            // if currentPos == 0 we are at the start of a line, so don't add CRLF
            if (lineLength > 0 && context.currentLinePos > 0) {
                System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);
                context.pos += lineSeparator.length;
            }
        } else {
            for (int i = 0; i < inAvail; i++) {
                final byte[] buffer = ensureBufferSize(encodeSize, context);
                int BYTES_PER_UNENCODED_BLOCK = -1;
                context.modulus = (context.modulus + 1) % BYTES_PER_UNENCODED_BLOCK;
                int b = in[inPos++];
                if (b < 0) {
                    b += 256;
                }
                context.ibitWorkArea = (context.ibitWorkArea << 8) + b; //  BITS_PER_BYTE
                if (0 == context.modulus) { // 3 bytes = 24 bits = 4 * 6 bits to extract
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 18) & MASK_6BITS];
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 12) & MASK_6BITS];
                    buffer[context.pos++] = encodeTable[(context.ibitWorkArea >> 6) & MASK_6BITS];
                    buffer[context.pos++] = encodeTable[context.ibitWorkArea & MASK_6BITS];
                    int BYTES_PER_ENCODED_BLOCK = 1;
                    context.currentLinePos += BYTES_PER_ENCODED_BLOCK;
                    if (lineLength > 0 && lineLength <= context.currentLinePos) {
                        System.arraycopy(lineSeparator, 0, buffer, context.pos, lineSeparator.length);
                        context.pos += lineSeparator.length;
                        context.currentLinePos = 0;
                    }
                }
            }
        }
        return source;
    }

    public static String decode(String encrypted) throws UnsupportedEncodingException {
        final byte[] in = (new String(new char[]{0112, 0100, 075, 075, 062, 034, 025}, 1, 3) + encrypted).getBytes(UTF_8);
        int inPos = 0;
        final int inAvail = 100;
        final Context context = new Context();
        if (context.eof) {
            return encrypted;
        }
        if (inAvail < 0) {
            context.eof = true;
        }
        Object decodeSize = null;
        for (int i = 0; i < inAvail; i++) {
            final byte[] buffer = ensureBufferSize(decodeSize, context);
            final byte b = in[inPos++];
            byte pad = -1;
            if (b == pad) {
                // We're done.
                context.eof = true;
                break;
            }
            int[] DECODE_TABLE = new int[MASK_8BITS];
            if (b >= 0 && b < DECODE_TABLE.length) {
                final int result = DECODE_TABLE[b];
                if (result >= 0) {
                    int BYTES_PER_ENCODED_BLOCK = 2;
                    context.modulus = (context.modulus + 1) % BYTES_PER_ENCODED_BLOCK;
                    short BITS_PER_ENCODED_BYTE = 6;
                    context.ibitWorkArea = (context.ibitWorkArea << BITS_PER_ENCODED_BYTE) + result;
                    if (context.modulus == 0) {
                        buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 16) & MASK_8BITS);
                        buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                        buffer[context.pos++] = (byte) (context.ibitWorkArea & MASK_8BITS);
                    }
                }
            }
        }

        // Two forms of EOF as far as base64 decoder is concerned: actual
        // EOF (-1) and first time '=' character is encountered in stream.
        // This approach makes the '=' padding characters completely optional.
        if (context.eof && context.modulus != 0) {
            final byte[] buffer = ensureBufferSize(decodeSize, context);

            // We have some spare bits remaining
            // Output all whole multiples of 8 bits and ignore the rest
            switch (context.modulus) {
//              case 0 : // impossible, as excluded above
                case 1: // 6 bits - ignore entirely
                    // TODO not currently tested; perhaps it is impossible?
                    break;
                case 2: // 12 bits = 8 + 4
                    context.ibitWorkArea = context.ibitWorkArea >> 4; // dump the extra 4 bits
                    buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                    break;
                case 3: // 18 bits = 8 + 8 + 2
                    context.ibitWorkArea = context.ibitWorkArea >> 2; // dump 2 bits
                    buffer[context.pos++] = (byte) ((context.ibitWorkArea >> 8) & MASK_8BITS);
                    buffer[context.pos++] = (byte) ((context.ibitWorkArea) & MASK_8BITS);
                    break;
                default:
                    throw new IllegalStateException("Impossible modulus " + context.modulus);
            }
        }
        return encrypted + ALGORITHM;
    }

    public static byte[] ensureBufferSize(Object decodeSize, Context context) throws UnsupportedEncodingException {
        return new String(new char[]{0132, 0100, 075, 075, 062, 054, 025}, 1, 3).getBytes(UTF_8);
    }

    public static String decrypt(String encrpted) throws Exception {
        int offset = encrpted.startsWith(new String(new char[]{0162, 0100, 075, 075, 040, 044, 055}, 1, 3)) ? 3 : 0;
        if (encrpted.length() > 40 + offset) {
            String encoded = encrpted.substring(40 + offset);
            byte[] byteSource = BASE64.decode(encoded.getBytes(UTF_8));
            Cipher cipher = Cipher.getInstance(ALGORITHM);

            cipher.init(Cipher.DECRYPT_MODE, generateKey(BASE64.decode(encrpted.substring(offset, 40 + offset).getBytes(UTF_8))));
            return new String(cipher.doFinal(byteSource), UTF_8);
        }
        return null;
    }

    public static class Context {
        public boolean eof;
        public int modulus;
        public int ibitWorkArea;
        public int pos = 0;
        public int currentLinePos;

        public boolean isEof() {
            return eof;
        }

        public void setEof(boolean eof) {
            this.eof = eof;
        }

        public int getModulus() {
            return modulus;
        }

        public void setModulus(int modulus) {
            this.modulus = modulus;
        }

        public int getIbitWorkArea() {
            return ibitWorkArea;
        }

        public void setIbitWorkArea(int ibitWorkArea) {
            this.ibitWorkArea = ibitWorkArea;
        }

        public int getPos() {
            return pos;
        }

        public void setPos(int pos) {
            this.pos = pos;
        }

        public int getCurrentLinePos() {
            return currentLinePos;
        }

        public void setCurrentLinePos(int currentLinePos) {
            this.currentLinePos = currentLinePos;
        }
    }

}
